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

View File

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

View File

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

View File

@ -654,6 +654,13 @@ export function updater(update: Update) {
id: buildApiPeerId(update.chatId, 'chat'), id: buildApiPeerId(update.chatId, 'chat'),
deletedMemberId: buildApiPeerId(update.userId, 'user'), 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 ( } else if (
update instanceof GramJs.UpdatePinnedMessages update instanceof GramJs.UpdatePinnedMessages
|| update instanceof GramJs.UpdatePinnedChannelMessages || update instanceof GramJs.UpdatePinnedChannelMessages

View File

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

View File

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

View File

@ -179,6 +179,13 @@ export type ApiUpdateChatMembers = {
deletedMemberId?: string; deletedMemberId?: string;
}; };
export type ApiUpdateChatParticipantRank = {
'@type': 'updateChatParticipantRank';
id: string;
userId: string;
rank: string;
};
export type ApiUpdatePinnedChatIds = { export type ApiUpdatePinnedChatIds = {
'@type': 'updatePinnedChatIds'; '@type': 'updatePinnedChatIds';
ids?: string[]; ids?: string[];
@ -924,7 +931,8 @@ export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
ApiUpdateChat | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds | ApiUpdateChat | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds |
ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds | ApiUpdateChatMembers | ApiUpdateChatParticipantRank | ApiUpdateChatJoin | ApiUpdateChatLeave
| ApiUpdateChatPinned | ApiUpdatePinnedMessageIds |
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders | ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdatePasskeyOption | ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdatePasskeyOption |
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory | 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"; "UserRestrictionsNoChangeInfo" = "can't change Info";
"UserRestrictionsInviteUsers" = "Add Users"; "UserRestrictionsInviteUsers" = "Add Users";
"UserRestrictionsPinMessages" = "Pin Messages"; "UserRestrictionsPinMessages" = "Pin Messages";
"UserRestrictionsEditRank" = "Edit Own Tags";
"ChatPermissionNotAvailable" = "This permission is not available in public groups."; "ChatPermissionNotAvailable" = "This permission is not available in public groups.";
"StatsMessageInteractionsTitle" = "INTERACTIONS"; "StatsMessageInteractionsTitle" = "INTERACTIONS";
"StatsGroupGrowthTitle" = "GROWTH"; "StatsGroupGrowthTitle" = "GROWTH";
@ -541,6 +542,7 @@
"PrivacyExceptions" = "Exceptions"; "PrivacyExceptions" = "Exceptions";
"AlwaysAllow" = "Always Allow"; "AlwaysAllow" = "Always Allow";
"EditAdminAddUsers" = "Add Users"; "EditAdminAddUsers" = "Add Users";
"EditAdminEditRank" = "Edit Member Tags";
"NeverAllow" = "Never Allow"; "NeverAllow" = "Never Allow";
"AlwaysAllowPlaceholder" = "Always allow..."; "AlwaysAllowPlaceholder" = "Always allow...";
"NeverAllowPlaceholder" = "Never allow..."; "NeverAllowPlaceholder" = "Never allow...";
@ -966,7 +968,7 @@
"StartVoipChatPermission" = "Manage Video Chats"; "StartVoipChatPermission" = "Manage Video Chats";
"EditAdminSendAnonymously" = "Remain Anonymous"; "EditAdminSendAnonymously" = "Remain Anonymous";
"ChannelEditAdminCannotEdit" = "You can't edit the rights of this admin."; "ChannelEditAdminCannotEdit" = "You can't edit the rights of this admin.";
"EditAdminRank" = "Custom title"; "EditAdminRank" = "Member tag";
"EditAdminRemoveAdmin" = "Dismiss Admin"; "EditAdminRemoveAdmin" = "Dismiss Admin";
"EditAdminTransferChannelOwnership" = "Transfer Channel Ownership"; "EditAdminTransferChannelOwnership" = "Transfer Channel Ownership";
"EditAdminTransferGroupOwnership" = "Transfer Group Ownership"; "EditAdminTransferGroupOwnership" = "Transfer Group Ownership";
@ -2732,4 +2734,19 @@
"GiftPreviewToggleRegularModels" = "View Primary Models >"; "GiftPreviewToggleRegularModels" = "View Primary Models >";
"AriaGiftPreviewPlay" = "Play random previews"; "AriaGiftPreviewPlay" = "Play random previews";
"AriaGiftPreviewStop" = "Pause 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"; "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 WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal';
export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal'; export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal';
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal'; 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 QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal'; export { default as StealthModeModal } from '../components/modals/storyStealthMode/StealthModeModal';
export { default as LeaveGroupModal } from '../components/modals/leaveGroup/LeaveGroupModal'; export { default as LeaveGroupModal } from '../components/modals/leaveGroup/LeaveGroupModal';

View File

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

View File

@ -1,5 +1,5 @@
.root { .root {
padding: 0.25em 0.5em; padding: 0.25rem 0.5rem;
border-radius: 1em; border-radius: 1em;
font-size: 0.75rem; font-size: 0.75rem;
@ -12,6 +12,11 @@
transition: 150ms filter ease-in; transition: 150ms filter ease-in;
} }
.plain {
color: rgba(var(--color-text-meta-rgb), 0.75);
background-color: transparent;
}
.clickable { .clickable {
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
@ -19,3 +24,7 @@
filter: brightness(1.1); 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 buildClassName from '../../util/buildClassName';
import styles from './BadgeButton.module.scss'; import styles from './BadgeButton.module.scss';
type OwnProps = { type OwnProps = {
children: React.ReactNode; children: TeactNode;
className?: string; className?: string;
isPlain?: boolean;
inline?: boolean;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void; onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void; onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
}; };
@ -14,12 +16,20 @@ type OwnProps = {
const BadgeButton = ({ const BadgeButton = ({
children, children,
className, className,
isPlain,
inline,
onClick, onClick,
onMouseDown, onMouseDown,
}: OwnProps) => { }: OwnProps) => {
return ( return (
<div <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} onClick={onClick}
onMouseDown={onMouseDown} 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'; } from '../../global/selectors';
import { selectThreadMessagesCount } from '../../global/selectors/threads'; import { selectThreadMessagesCount } from '../../global/selectors/threads';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { hasRank } from './helpers/chatMember';
import { REM } from './helpers/mediaDimensions'; import { REM } from './helpers/mediaDimensions';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
@ -33,6 +34,7 @@ import Avatar from './Avatar';
import DotAnimation from './DotAnimation'; import DotAnimation from './DotAnimation';
import FullNameTitle from './FullNameTitle'; import FullNameTitle from './FullNameTitle';
import Icon from './icons/Icon'; import Icon from './icons/Icon';
import RankBadge from './RankBadge';
import TopicIcon from './TopicIcon'; import TopicIcon from './TopicIcon';
import TypingStatus from './TypingStatus'; import TypingStatus from './TypingStatus';
@ -58,7 +60,8 @@ type BaseOwnProps = {
emojiStatusSize?: number; emojiStatusSize?: number;
noStatusOrTyping?: boolean; noStatusOrTyping?: boolean;
noRtl?: boolean; noRtl?: boolean;
adminMember?: ApiChatMember; chatMemberOriginId?: string;
chatMember?: ApiChatMember;
isSavedDialog?: boolean; isSavedDialog?: boolean;
noAvatar?: boolean; noAvatar?: boolean;
className?: string; className?: string;
@ -118,7 +121,8 @@ const PrivateChatInfo = ({
isSavedMessages, isSavedMessages,
isSavedDialog, isSavedDialog,
areMessagesLoaded, areMessagesLoaded,
adminMember, chatMember,
chatMemberOriginId,
ripple, ripple,
className, className,
storyViewerOrigin, storyViewerOrigin,
@ -237,10 +241,6 @@ const PrivateChatInfo = ({
); );
} }
const customTitle = adminMember
? adminMember.customTitle || oldLang(adminMember.isOwner ? 'GroupInfo.LabelOwner' : 'GroupInfo.LabelAdmin')
: undefined;
function renderNameTitle() { function renderNameTitle() {
if (isTopic) { if (isTopic) {
return ( return (
@ -248,18 +248,27 @@ const PrivateChatInfo = ({
); );
} }
if (customTitle) { if (chatMember && hasRank(chatMember)) {
return ( return (
<div className="info-name-title"> <div className="info-name-title">
<FullNameTitle <FullNameTitle
peer={user!} peer={customPeer || user!}
noFake={noFake}
noVerified={noVerified}
withEmojiStatus={!noEmojiStatus} withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize} emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog} isSavedDialog={isSavedDialog}
iconElement={iconElement}
onEmojiStatusClick={onEmojiStatusClick} 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> </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 <img src={locationBlobUrl} alt="" className={styles.businessLocation} />;
} }
return <Skeleton className={styles.businessLocation} />; return <Skeleton className={styles.businessLocation} animation="wave" />;
}, [businessLocation, locationBlobUrl]); }, [businessLocation, locationBlobUrl]);
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID); const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);

View File

@ -249,6 +249,17 @@ const PermissionCheckboxList = ({
onChange={handlePermissionChange} onChange={handlePermissionChange}
/> />
</div> </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 <div
className={buildClassName('ListItem', withCheckbox && 'with-checkbox')} className={buildClassName('ListItem', withCheckbox && 'with-checkbox')}
onClick={shouldDisablePermissionForPublicGroup ? handleDisabledClick : undefined} onClick={shouldDisablePermissionForPublicGroup ? handleDisabledClick : undefined}

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ function LastEditTimeMenuItem({
return ( return (
<MenuItem icon="clock-edit" className={styles.item}> <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))} && lang('Chat.PrivateMessageEditTimestamp.Date', formatDateAtTime(lang, editDate * 1000))}
</MenuItem> </MenuItem>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -303,7 +303,6 @@
user-select: none; user-select: none;
unicode-bidi: plaintext; unicode-bidi: plaintext;
overflow: hidden;
display: flex; display: flex;
font-size: calc(var(--message-text-size, 1rem) - 0.125rem); font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
@ -399,11 +398,12 @@
flex-grow: 1; flex-grow: 1;
} }
.admin-title { .admin-title-badge, .admin-title {
user-select: none; user-select: none;
margin-left: 1rem; margin-left: 1rem;
}
.admin-title {
font-size: 0.75rem; font-size: 0.75rem;
font-weight: var(--font-weight-normal); font-weight: var(--font-weight-normal);
color: rgba(var(--color-text-meta-rgb), 0.75); color: rgba(var(--color-text-meta-rgb), 0.75);
@ -414,6 +414,10 @@
} }
} }
.admin-title-badge {
margin-right: -0.1875rem;
}
.sender-boosts { .sender-boosts {
user-select: none; 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 { &.audio {
min-width: 20rem; min-width: 20rem;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -77,3 +77,23 @@ addActionHandler('openProfileRatingModal', (global, actions, payload): ActionRet
}); });
addTabStateResetterAction('closeProfileRatingModal', 'profileRatingModal'); 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>( export function replaceSimilarChannels<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,

View File

@ -1,8 +1,8 @@
import type {
ApiChat, ApiChatFullInfo, ApiChatType,
} from '../../api/types';
import type { ChatListType } from '../../types'; import type { ChatListType } from '../../types';
import type { GlobalState, TabArgs } from '../types'; import type { GlobalState, TabArgs } from '../types';
import {
type ApiChat, type ApiChatFullInfo, type ApiChatType,
} from '../../api/types';
import { import {
ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID, 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 { getCurrentTabId } from '../../util/establishMultitabRole';
import { import {
getHasAdminRight, getHasAdminRight,
isChatAdmin,
isChatChannel, isChatChannel,
isChatPublic, isChatPublic,
isChatSuperGroup, isChatSuperGroup,
isHistoryClearMessage, isHistoryClearMessage,
isUserBot, isUserBot,
isUserOnline, isUserOnline,
isUserRightBanned,
} from '../helpers'; } from '../helpers';
import { selectActiveRestrictionReasons } from './messages'; import { selectActiveRestrictionReasons } from './messages';
import { selectTabState } from './tabs'; import { selectTabState } from './tabs';
@ -378,3 +380,35 @@ export function selectAreFoldersPresent<T extends GlobalState>(global: T) {
const ids = global.chatFolders.orderedIds; const ids = global.chatFolders.orderedIds;
return Boolean(ids && ids.length > 1); 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; chatId: string;
userId: string; userId: string;
adminRights: ApiChatAdminRights; adminRights: ApiChatAdminRights;
customTitle?: string; rank?: string;
} & WithTabId; } & WithTabId;
editChatParticipantRank: {
chatId: string;
userId: string;
rank: string;
};
checkChatInvite: { checkChatInvite: {
hash: string; hash: string;
@ -1938,6 +1943,22 @@ export interface ActionPayloads {
level: number; level: number;
} & WithTabId; } & WithTabId;
closeProfileRatingModal: WithTabId | undefined; 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: { loadMoreProfilePhotos: {
peerId: string; peerId: string;
isPreload?: boolean; isPreload?: boolean;

View File

@ -1035,5 +1035,19 @@ export type TabState = {
isCocoonModalOpen?: boolean; 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; 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.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.editChatCreator#f743b857 peer:InputPeer user_id:InputUser password:InputCheckPasswordSRP = Updates;
messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User; messages.getFutureChatCreatorAfterLeave#3b7d0ea6 peer:InputPeer = User;
messages.editChatParticipantRank#a00f32b0 peer:InputPeer participant:InputPeer rank:string = Updates;
updates.getState#edd4882a = updates.State; 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.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; 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.summarizeText",
"messages.editChatCreator", "messages.editChatCreator",
"messages.getFutureChatCreatorAfterLeave", "messages.getFutureChatCreatorAfterLeave",
"messages.editChatParticipantRank",
"updates.getState", "updates.getState",
"updates.getDifference", "updates.getDifference",
"updates.getChannelDifference", "updates.getChannelDifference",

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -1,323 +1,324 @@
export type FontIconName = 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' | 'active-sessions'
| 'rating-icons-negative' | 'add-caption'
| 'rating-icons-level90' | 'add-filled'
| 'rating-icons-level9' | 'add-one-badge'
| 'rating-icons-level80' | 'add-user-filled'
| 'rating-icons-level8' | 'add-user'
| 'rating-icons-level70' | 'add'
| 'rating-icons-level7' | 'admin'
| 'rating-icons-level60' | 'allow-speak'
| 'rating-icons-level6' | 'animals'
| 'rating-icons-level50' | 'animations'
| 'rating-icons-level5' | 'archive-filled'
| 'rating-icons-level40' | 'archive-from-main'
| 'rating-icons-level4' | 'archive-to-main'
| 'rating-icons-level30' | 'archive'
| 'rating-icons-level3' | 'arrow-down-circle'
| 'rating-icons-level20' | 'arrow-down'
| 'rating-icons-level2' | 'arrow-left'
| 'rating-icons-level10' | 'arrow-right'
| 'rating-icons-level1' | 'ask-support'
| 'folder-tabs-user' | 'attach'
| 'folder-tabs-star' | 'auction-drop'
| 'folder-tabs-group' | 'auction-filled'
| 'folder-tabs-folder' | 'auction-next-round'
| 'folder-tabs-chats' | 'auction'
| 'folder-tabs-chat' | '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-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; 'UserRestrictionsNoChangeInfo': undefined;
'UserRestrictionsInviteUsers': undefined; 'UserRestrictionsInviteUsers': undefined;
'UserRestrictionsPinMessages': undefined; 'UserRestrictionsPinMessages': undefined;
'UserRestrictionsEditRank': undefined;
'ChatPermissionNotAvailable': undefined; 'ChatPermissionNotAvailable': undefined;
'StatsMessageInteractionsTitle': undefined; 'StatsMessageInteractionsTitle': undefined;
'StatsGroupGrowthTitle': undefined; 'StatsGroupGrowthTitle': undefined;
@ -490,6 +491,7 @@ export interface LangPair {
'PrivacyExceptions': undefined; 'PrivacyExceptions': undefined;
'AlwaysAllow': undefined; 'AlwaysAllow': undefined;
'EditAdminAddUsers': undefined; 'EditAdminAddUsers': undefined;
'EditAdminEditRank': undefined;
'NeverAllow': undefined; 'NeverAllow': undefined;
'AlwaysAllowPlaceholder': undefined; 'AlwaysAllowPlaceholder': undefined;
'NeverAllowPlaceholder': undefined; 'NeverAllowPlaceholder': undefined;
@ -2004,6 +2006,17 @@ export interface LangPair {
'GiftPreviewToggleRegularModels': undefined; 'GiftPreviewToggleRegularModels': undefined;
'AriaGiftPreviewPlay': undefined; 'AriaGiftPreviewPlay': undefined;
'AriaGiftPreviewStop': 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; 'MenuAddCaption': undefined;
} }
@ -3519,6 +3532,24 @@ export interface LangPairWithVariables<V = LangVariable> {
'BotAuthSuccessText': { 'BotAuthSuccessText': {
'url': V; '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 { export interface LangPairPlural {